package com.navercorp.utilset.network;

import com.navercorp.utilset.utils.UtilSetLogUtils;

import ohos.aafwk.ability.DataAbilityHelper;
import ohos.aafwk.content.Intent;
import ohos.aafwk.content.IntentParams;
import ohos.app.Context;
import ohos.data.DatabaseHelper;
import ohos.data.preferences.Preferences;
import ohos.dcall.CallStateObserver;
import ohos.dcall.DistributedCallManager;
import ohos.event.commonevent.CommonEventData;
import ohos.event.commonevent.CommonEventManager;
import ohos.event.commonevent.CommonEventSubscribeInfo;
import ohos.event.commonevent.CommonEventSubscriber;
import ohos.event.commonevent.MatchingSkills;
import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;
import ohos.net.ConnectionProperties;
import ohos.net.LinkAddress;
import ohos.net.NetHandle;
import ohos.net.NetManager;
import ohos.rpc.RemoteException;
import ohos.sysappcomponents.settings.SystemSettings;
import ohos.telephony.CellularDataInfoManager;
import ohos.telephony.NetworkState;
import ohos.telephony.RadioInfoManager;
import ohos.telephony.RadioStateObserver;
import ohos.telephony.SimInfoManager;
import ohos.wifi.WifiDevice;
import ohos.wifi.WifiEvents;
import ohos.wifi.WifiLinkedInfo;

import java.io.IOException;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.nio.channels.UnresolvedAddressException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

/**
 * This class provides network related methods In order to use this class, some
 * permissions such as READ_PHONE_STATE, ACCESS_NETWORK_STATE are required
 *
 * @author jaemin.woo
 */
public class NetworkMonitor {
    private static final String KEY_ROAMING_ON = "KEY_ROAMING_ON";

    private static final String TAG = "NetworkUtils";

    private static NetworkMonitor instance = null;

    private static boolean isWifiConnectedPrevious = false;

    private static final int TYPE_MOBILE = 0;

    private static final int TYPE_WIFI = 1;

    private static final int TYPE_MOBILE_MMS = 2;

    private static final int TYPE_MOBILE_SUPL = 3;

    private static final int TYPE_MOBILE_DUN = 4;

    private static final int TYPE_MOBILE_HIPRI = 5;

    private static final int TYPE_WIMAX = 6;

    private static final int TYPE_BLUETOOTH = 7;

    private static final int TYPE_DUMMY = 8;

    private static final int TYPE_ETHERNET = 9;

    private static final int STATE_NONE = -1;

    private Context context;

    private ArrayList<NetworkStateChangedListener> listeners;

    private ArrayList<NetworkConnectedListener> connectedListeners;

    private ArrayList<PhoneCalledListener> callListeners;

    private EventHandler handler;

    private Runnable networkChangedRunnable;

    private Runnable networkConnectedRunable;

    private CommonEventSubscriber wifiEventSubscriber = null;

    private CallStateObserver callStateObserver = null;

    private NetworkMonitor(Context context) {
        this.context = context;
        handler = new EventHandler(EventRunner.current());
        listeners = new ArrayList<>();
        connectedListeners = new ArrayList<>();
        callListeners = new ArrayList<>();

        networkChangedRunnable = () -> {
            synchronized (NetworkMonitor.this) {
                if (listeners == null) {
                    return;
                }
                for (NetworkStateChangedListener stateChangedListener : listeners) {
                    stateChangedListener.onNetworkStateChanged();
                }
            }
        };

        networkConnectedRunable = () -> {
            synchronized (NetworkMonitor.this) {
                if (connectedListeners == null) {
                    return;
                }
                for (NetworkConnectedListener connectedListener : connectedListeners) {
                    connectedListener.onNetworkConnected();
                }
            }
        };

        registerConnectivityReceiver();

        callStateObserver = new CallStateObserver(0) {
            @Override
            public void onCallStateUpdated(int state, String number) {
                super.onCallStateUpdated(state, number);
                handleOnCallStateChanged(state);
            }
        };
        DistributedCallManager.getInstance(context)
                .addObserver(callStateObserver, CallStateObserver.OBSERVE_MASK_CALL_STATE);

        setWifiConnectedPreviously(isWifiConnected());
    }

    /**
     * getInstance
     *
     * @param context Context to be used to get network information.<br>
     *                To avoid memory leak, pass Application Context as argument
     * @return NetworkMonitor instance
     */
    public static NetworkMonitor getInstance(Context context) {
        if (instance == null) {
            instance = new NetworkMonitor(context);
        }
        return instance;
    }

    /**
     * destroy
     */
    public static void destroy() {
        if (instance == null) {
            return;
        }
        instance.unregisterConnectivityReceiver();
        instance.clearPhoneCalledListener();
        instance.clearWifiStateChangedListener();
        instance.clearNetworkConnectedListener();
        instance = null;
    }

    /**
     * NetworkStateChangedListener interface
     */
    public interface NetworkStateChangedListener {
        void onNetworkStateChanged();
    }

    /**
     * NetworkConnectedListener interface
     */
    public interface NetworkConnectedListener {
        void onNetworkConnected();
    }

    /**
     * PhoneCalledListener interface
     */
    public interface PhoneCalledListener {
        void onPhoneCallStateChanged(int state);
    }

    private void handleOnCallStateChanged(final int state) {
        handler.postTask(() -> {
            synchronized (NetworkMonitor.this) {
                if (callListeners == null) {
                    return;
                }
                for (PhoneCalledListener calledListener : callListeners) {
                    calledListener.onPhoneCallStateChanged(state);
                }
            }
        });
    }

    /**
     * addPhoneCalledListener
     * This function is called when the device has a phone call
     *
     * @param listener PhoneCalledListener which implements IPhoneCalledListener
     * @return true if already that listener is registered; false otherwise
     */
    public boolean addPhoneCalledListener(PhoneCalledListener listener) {
        synchronized (this) {
            if (callListeners.contains(listener)) {
                return true;
            }
            return callListeners.add(listener);
        }
    }

    /**
     * Removes Phone Call Listener<br>
     *
     * @param listener Listener to be removed
     * @return true if it succeeds to remove listener; false otherwise;
     */
    public boolean removePhoneCalledListener(PhoneCalledListener listener) {
        synchronized (this) {
            if (callListeners.size() == 0) {
                return false;
            }
            return callListeners.remove(listener);
        }
    }

    private void clearPhoneCalledListener() {
        synchronized (this) {
            DistributedCallManager.getInstance(context).removeObserver(callStateObserver);
            callListeners.clear();
        }
    }

    private void handleNetworkChanged() {
        handler.postTask(networkChangedRunnable);
    }

    private void handleNetworkConnected() {
        handler.postTask(networkConnectedRunable);
    }

    /**
     * Registers receiver to be notified of broadcast event when network
     * connection changes
     */
    private void registerConnectivityReceiver() {
        MatchingSkills filter = new MatchingSkills();
        filter.addEvent(WifiEvents.EVENT_ACTIVE_STATE);

        CommonEventSubscribeInfo subscribeInfo = new CommonEventSubscribeInfo(filter);
        wifiEventSubscriber = new CommonEventSubscriber(subscribeInfo) {
            @Override
            public void onReceiveEvent(CommonEventData commonEventData) {
                Intent intent = commonEventData.getIntent();
                if (intent == null) {
                    return;
                }
                handleNetworkChanged();
            }
        };
        try {
            CommonEventManager.subscribeCommonEvent(wifiEventSubscriber);
        } catch (RemoteException e) {
            UtilSetLogUtils.error(TAG, "Exception in subscribe Wifi Events");
        }
    }

    /**
     * Removes receiver to be notified broadcast event
     */
    private void unregisterConnectivityReceiver() {
        try {
            CommonEventManager.unsubscribeCommonEvent(wifiEventSubscriber);
        } catch (RemoteException e) {
            UtilSetLogUtils.error(TAG, "Unsubscribe wifi event failed");
        }
    }

    /**
     * Adds WifiStateChangedListener<br>
     * This function is called when the device has a phone call
     *
     * @param listener Listener which implements IPhoneCalledListener
     * @return true if already that listener is registered; false otherwise
     */
    public boolean addWifiStateChangedListener(NetworkStateChangedListener listener) {
        synchronized (this) {
            if (listeners.contains(listener)) {
                return true;
            }
            return listeners.add(listener);
        }
    }

    /**
     * Removes WifiStateChangedListener<br>
     *
     * @param listener Listener to be removed
     * @return true if it succeeds to remove listener; false otherwise
     */

    public boolean removeWifiStateChangedListener(NetworkStateChangedListener listener) {
        synchronized (this) {
            if (listeners.size() == 0) {
                return false;
            }
            return listeners.remove(listener);
        }
    }

    private void clearWifiStateChangedListener() {
        synchronized (this) {
            listeners.clear();
        }
    }

    /**
     * Registers Network Connection Listener<br>
     * Once registered, connection listeners can be alerted when the device
     * comes to have network connection
     *
     * @param listener Listener which implements INetworkConnectedListener
     * @return true if already that listener is registered; false otherwise
     */
    public boolean addNetworkConnectedListener(NetworkConnectedListener listener) {
        synchronized (this) {
            if (connectedListeners.contains(listener)) {
                return true;
            }
            return connectedListeners.add(listener);
        }
    }

    /**
     * Removes Phone Call Listener
     *
     * @param listener Listener to be removed
     * @return true if it succeeds to remove listener; false otherwise
     */
    public boolean removeNetworkConnectedListener(NetworkConnectedListener listener) {
        synchronized (this) {
            if (connectedListeners.size() == 0) {
                return false;
            }
            return connectedListeners.remove(listener);
        }
    }

    private void clearNetworkConnectedListener() {
        synchronized (this) {
            connectedListeners.clear();
        }
    }

    /**
     * Returns WiFi connection state<br>
     * Requires ACCESS_NETWORK_SATE, READ_PHONE_STATE permissions<br>
     *
     * @return true if WiFi is connected; false otherwise
     */
    public boolean isWifiConnected() {
        WifiDevice wifiDevice = WifiDevice.getInstance(context);
        return wifiDevice.isConnected();
    }

    /**
     * Checks if WiFi is turned on<br>
     * Requires ACCESS_NETWORK_SATE, READ_PHONE_STATE permissions<br>
     *
     * @return true if WiFi is turned on; false otherwise
     */
    public boolean isWifiEnabled() {
        WifiDevice wifiDevice = WifiDevice.getInstance(context);
        return wifiDevice.isWifiActive();
    }

    /**
     * Check if Mobile network is connected<br>
     * Requires GET_NETWORK_INFO permissions<br>
     *
     * @return true if Mobile network has connection; false otherwise
     */
    public boolean isMobileConnected() {
        CellularDataInfoManager cdim = CellularDataInfoManager.getInstance(context);
        int slotId = cdim.getDefaultCellularDataSlotId();
        int dataState = CellularDataInfoManager.getInstance(context).getCellularDataState(slotId);

        return dataState == CellularDataInfoManager.DATA_STATE_CONNECTED;
    }

    /**
     * Checks if Mobile Network is turned on<br>
     * Requires GET_NETWORK_INFO permissions<br>
     *
     * @return true if Mobile Network is turned on; false otherwise
     */
    public boolean getMobileState() {
        return CellularDataInfoManager.getInstance(context).isCellularDataEnabled();
    }

    /**
     * Returns WiFi State Requires ACCESS_NETWORK_SATE, READ_PHONE_STATE
     * permissions<br>
     *
     * @return State
     */
    public WifiLinkedInfo.ConnState getWifiState() {
        WifiDevice wifiDevice = WifiDevice.getInstance(context);
        Optional<WifiLinkedInfo> info = wifiDevice.getLinkedInfo();

        WifiLinkedInfo.ConnState state = WifiLinkedInfo.ConnState.UNKNOWN;
        if (info.isPresent()) {
            WifiLinkedInfo linkedInfo = info.get();
            state = linkedInfo.getConnState();
        }
        return state;
    }

    /**
     * Tells if network is currently connected<br>
     * Requires ACCESS_NETWORK_SATE, READ_PHONE_STATE permissions<br>
     *
     * @return true if connected; false otherwise
     */
    public boolean isNetworkConnected() {
        try {
            return isMobileConnected() || isWifiConnected();
        } catch (Exception e) {
            UtilSetLogUtils.error(TAG, "Exception during isMobileConnected(). - " + e.getLocalizedMessage());
        }
        return false;
    }

    /**
     * Checks if airplane mode is on<br>
     * Requires ACCESS_NETWORK_SATE, READ_PHONE_STATE permissions<br>
     *
     * @return true if airplane mode is on; false otherwise
     */
    public boolean isAirplaneModeOn() {
        DataAbilityHelper resolver = DataAbilityHelper.creator(context);
        String status = SystemSettings.getValue(resolver, SystemSettings.General.AIRPLANE_MODE_STATUS);
        return status.equalsIgnoreCase("1");
    }

    /**
     * Returns SIM state<br>
     * Requires ACCESS_NETWORK_SATE, READ_PHONE_STATE permissions<br>
     *
     * @return SIM State (Refer DEV Guide, SimInfoManager)
     */
    public int getSimState() {
        SimInfoManager simInfoManager = SimInfoManager.getInstance(context);
        return simInfoManager.getSimState(simInfoManager.getDefaultVoiceSlotId());
    }

    /**
     * listenRoamingState
     */
    public void listenRoamingState() {
        RadioInfoManager.getInstance(context).addObserver(new RadioStateObserver(0) {
            @Override
            public void onNetworkStateUpdated(NetworkState networkState) {
                super.onNetworkStateUpdated(networkState);

                final int state = networkState.getRegState();
                if (state == NetworkState.REG_STATE_IN_SERVICE || state == NetworkState.REG_STATE_POWER_OFF) {
                    final boolean roamingState = networkState.isRoaming();
                    if (roamingState != isRoamingOn()) {
                        setRoamingOn(roamingState);
                    }
                }
            }
        }, RadioStateObserver.OBSERVE_MASK_NETWORK_STATE);
    }

    private Preferences getApplicationPref() {
        DatabaseHelper databaseHelper = new DatabaseHelper(context);
        return databaseHelper.getPreferences("app_preference");
    }

    /**
     * Returns boolean
     *
     * @return isRoamingOn state
     */
    public boolean isRoamingOn() {
        Preferences preferences = getApplicationPref();
        return preferences.getBoolean(KEY_ROAMING_ON, false);
    }

    private void setRoamingOn(boolean isSetRoamingOn) {
        Preferences preferences = getApplicationPref();
        preferences.putBoolean(KEY_ROAMING_ON, isSetRoamingOn);
    }

    /**
     * setWifiConnectedPreviously
     *
     * @param isWifiConnected boolean
     */
    public void setWifiConnectedPreviously(boolean isWifiConnected) {
        isWifiConnectedPrevious = isWifiConnected;
    }

    /**
     * getWifiConnectedPreviously
     *
     * @return isWifiConnectedPrevious boolean
     */
    public boolean getWifiConnectedPreviously() {
        return isWifiConnectedPrevious;
    }

    /**
     * Returns IP Address<br>
     * Requires READ_PHONE_STATE, ACCESS_WIFI_STATE and INTERNET permissions<br>
     *
     * @param ipv6 Option to choose IP address type
     * @return IP address made up of IPv6 if parameter ipv6 is true or IPv4 if
     * parameter ipv6 is false; null if it do not have IP address
     * @throws RuntimeException when it fails due to poor network condition
     */
    public String getIpAddress(boolean ipv6) {
        NetManager netManager = NetManager.getInstance(context);
        NetHandle netHandle = netManager.getDefaultNet();
        if (netHandle != null) {
            ConnectionProperties prop = netManager.getConnectionProperties(netHandle);
            if (prop != null) {
                List<LinkAddress> linkAddresses = prop.getLinkAddresses();
                for (LinkAddress linkAddr : linkAddresses) {
                    InetAddress inetAddress = linkAddr.getAddress();

                    if (inetAddress.isLoopbackAddress()) {
                        continue;
                    }
                    if (ipv6 && inetAddress instanceof Inet6Address) {
                        return inetAddress.getHostAddress();
                    } else if (!ipv6 && inetAddress instanceof Inet4Address) {
                        return inetAddress.getHostAddress();
                    }
                }
            }
        }
        return "None";
    }

    /**
     * Returns MAC Address of WiFi Adapter<br>
     * Requires READ_PHONE_STATE, ACCESS_NETWORK_STATE and ACCESS_WIFI_STATE
     * permissions<br>
     *
     * @return String representing MAC Address of WiFi Adapter; Empty String in
     * some cases such as No WiFi Adapter is installed or WiFi is turned
     * off.
     */
    public String getWifiMacAddress() {
        String macAddress = "";
        WifiDevice wifiDevice = WifiDevice.getInstance(context);
        Optional<WifiLinkedInfo> linkedInfo = wifiDevice.getLinkedInfo();

        if (linkedInfo.isPresent()) {
            macAddress = linkedInfo.get().getMacAddress();
        }
        return macAddress;
    }

    /**
     * Turns on WiFi Adapter<br>
     * <p>
     * Requires READ_PHONE_STATE, ACCESS_NETWORK_STATE and CHANGE_WIFI_STATE
     * permissions<br>
     *
     * @param wifiContext Context
     */
    public void setWifiEnabled(Context wifiContext) {
        WifiDevice wifiDevice = WifiDevice.getInstance(wifiContext);
    }

    /**
     * Turns off WiFi Adapter<br>
     * <p>
     * Requires READ_PHONE_STATE, ACCESS_NETWORK_STATE and CHANGE_WIFI_STATE
     * permissions<br>
     *
     * @param wifiContext Context
     */
    public void setWifiDisabled(Context wifiContext) {
        WifiDevice wifiDevice = WifiDevice.getInstance(wifiContext);
    }

    /**
     * Checks if currently connected Access Point is either rogue or incapable
     * of routing packet
     *
     * @param ipAddress Any valid IP addresses will work to check if the device is
     *                  connected to properly working AP
     * @param port      Port number bound with ipAddress parameter
     * @return true if currently connected AP is not working normally; false
     * otherwise
     */
    public boolean isWifiFake(String ipAddress, int port) {
        SocketChannel socketChannel = null;
        try {
            socketChannel = SocketChannel.open();
            socketChannel.connect(new InetSocketAddress(ipAddress, port));
        } catch (IOException e) {
            UtilSetLogUtils.debug(TAG, "Current Wifi is stupid!!! by IOException");
            return true;
        } catch (UnresolvedAddressException e) {
            UtilSetLogUtils.debug(TAG, "Current Wifi is stupid!!! by InetSocketAddress");
            return true;
        } finally {
            if (socketChannel != null) {
                try {
                    socketChannel.close();
                } catch (IOException e) {
                    UtilSetLogUtils.debug(TAG, "Error occured while closing SocketChannel");
                }
            }
        }
        return false;
    }
}
